package jmine.tec.test.testcase;

import java.util.Arrays;

import jmine.tec.environment.db.DBEnvironment;
import jmine.tec.environment.db.DBEnvironmentHolder;
import jmine.tec.environment.db.DBLoadedServerUtils;
import jmine.tec.environment.utils.SpringConfigFactory;
import jmine.tec.environment.utils.TestDBApplicationContext;
import jmine.tec.test.TestMessages;
import jmine.tec.test.testcase.exception.DBException;

import org.apache.log4j.Logger;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.transaction.TransactionStatus;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.SystemEntityIdentifier;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.dao.SystemEntityDAO;
import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType;

/**
 * @created Sep 25, 2008
 * @author galmeida (Sep 25, 2008)
 */
public abstract class DBTestCase extends TestCase {

    private static final Logger LOG = Logger.getLogger(DBTestCase.class);

    private boolean testDataInitialized = false;

    /**
     * Construtor.
     */
    public DBTestCase() {
        super();
    }

    /**
     * Construtor.
     * 
     * @param name nome
     */
    public DBTestCase(String name) {
        super(name);
    }

    /**
     * Instancia um ambiente.
     * 
     * @return um ambiente.
     */
    protected DBEnvironment instantiateEnvironment() {
        SpringConfigFactory configFactory = new SpringConfigFactory();
        BeanFactory bf =
                new TestDBApplicationContext(this.getSpringMainXMLFilename(), configFactory.getApplicationContext()).getBeanFactory();
        return new DBEnvironment(bf, this.getSpringMainXMLFilename(), configFactory.getDatabaseServerType(),
                this.getResourcesBaseReferencia(), this.getReferenceDatabaseDescriptionType());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void oncePerVM() {
        super.oncePerVM();
        if (this.getEnvironment() == null) {
            this.setEnvironment(this.instantiateEnvironment());
            this.getEnvironment().start();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void oncePerTestCaseClass() {
        super.oncePerTestCaseClass();

        if (!this.getEnvironment().getEnvironmentKey().equals(this.getSpringMainXMLFilename())) {
            throw new DBException(TestMessages.DB_TEST_CASE_SPRING_CONFIG_FILE_CHANGED.create(this.getSpringMainXMLFilename(), this
                    .getEnvironment().getEnvironmentKey()));
        }

        if (!Arrays.equals(this.getResourcesBaseReferencia(), this.getHelper().getRefDbResources())) {
            StringBuilder arquivosBaseRefNovos = new StringBuilder("\"");
            String separator = "";
            for (String str : this.getResourcesBaseReferencia()) {
                arquivosBaseRefNovos.append(separator);
                arquivosBaseRefNovos.append(str);
                separator = ",";
            }

            StringBuilder arquivosBaseRefAntigos = new StringBuilder("\"");
            separator = "";
            for (String str : this.getHelper().getRefDbResources()) {
                arquivosBaseRefAntigos.append(separator);
                arquivosBaseRefAntigos.append(str);
                separator = ",";
            }

            throw new DBException(TestMessages.DB_TEST_CASE_REF_DB_RESOURCES_CHANGED.create(arquivosBaseRefNovos, arquivosBaseRefAntigos));
        }

        this.getEnvironment().setRefdbType(this.getReferenceDatabaseDescriptionType());
        this.getEnvironment().restart();
        this.executeInitializeTestData();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        if (!this.canOptimizeRestartBD()) {
            this.getEnvironment().markDirty();
        }
    }

    /**
     * Inicializa os dados para o teste apenas se eles ainda nao tiverem sido inicializados.
     */
    protected final void executeInitializeTestData() {
        if (!this.testDataInitialized) {
            TransactionStatus status = this.getHelper().getTransactionStatus();
            boolean commit = false;
            try {
                LOG.trace(this.getClass().getSimpleName() + ".executeInitializeTestData()");
                this.initializeTestData();
                this.testDataInitialized = true;
                commit = true;
            } catch (BeanNotFoundException e) {
                throw new DBException(TestMessages.DB_TEST_CASE_ERROR_INITIALIZING_TEST_DATA.create(), e);
            } finally {
                if (commit) {
                    this.getHelper().commit(status);
                } else {
                    this.getHelper().rollback(status);
                }
            }
        }
    }

    /**
     * Devolve o helper.
     * 
     * @return o helper.
     */
    public DBEnvironment getEnvironment() {
        return DBEnvironmentHolder.getInstance().getEnvironment();
    }

    /**
     * Define o environment.
     * 
     * @param environment o environment a ser definido.
     */
    protected void setEnvironment(DBEnvironment environment) {
        DBEnvironmentHolder.getInstance().setEnvironment(environment);
    }

    /**
     * Devolve o helper.
     * 
     * @return o helper.
     */
    public DBLoadedServerUtils getHelper() {
        return this.getEnvironment().getHelper();
    }

    /**
     * Devolve o controller.
     * 
     * @return o controller.
     * @see #getControllerSpringId()
     */
    protected PersistenceEnabledController getController() {
        final String notOnSpring = "bean " + this.getControllerSpringId() + " não está presente no spring";
        final String notPersistenceEnabledController = this.getControllerSpringId() + "não é um PersistenceEnabledController";
        assertNotNull("O nome do controller nao pode ser null", this.getControllerSpringId());
        assertTrue(notOnSpring, this.getHelper().getBeanFactory().containsBean(this.getControllerSpringId()));
        Object controller = this.getHelper().getBeanFactory().getBean(this.getControllerSpringId());
        assertTrue(notPersistenceEnabledController, controller instanceof PersistenceEnabledController);

        return (PersistenceEnabledController) controller;
    }

    /**
     * Método usado para reassicioar métodos à sessão do hibernate, pode ser usado em testes que tenham problemas com lazy.
     * 
     * @param objects objetos a serem reassociados.
     */
    protected void reassociateToSession(Object... objects) {
        Session session = this.getHelper().getSessionFactory().getCurrentSession();
        for (Object object : objects) {
            session.lock(object, LockMode.NONE);
        }
    }

    /**
     * @param <T> the Persistable type
     * @param clazz a {@link Persistable} class
     * @return a new instance of the specified type
     */
    protected <T extends Persistable> T createBean(Class<T> clazz) {
        return this.getController().getDAOFactory().getGenericDAO(clazz).createBean();
    }

    /**
     * @param <T> a type of {@link Persistable}
     * @param clazz a {@link Persistable} class
     * @param nk a natural key
     * @return the given object OR null
     */
    protected <T extends Persistable> T findByNaturalKey(Class<T> clazz, String nk) {
        BaseDAO<T> dao = this.getController().getDAOFactory().getGenericDAO(clazz);
        try {
            return dao.findByNaturalKey(nk);
        } catch (BeanNotFoundException e) {
            return null;
        }
    }

    /**
     * @param <T> a type of {@link Persistable}
     * @param <S> a type of {@link SystemEntityIdentifier}
     * @param daoClass a {@link SystemEntityDAO} class
     * @param si a {@link SystemEntityIdentifier}
     * @return the required object
     */
    protected <T extends Persistable, S extends SystemEntityIdentifier<?>> T findBySystemIdentifier(
            Class<? extends SystemEntityDAO<T, S>> daoClass, S si) {
        SystemEntityDAO<T, S> dao = this.getController().getDAOFactory().getDAOByClass(daoClass);
        return dao.findBySystemIdentifier(si);
    }

    /**
     * Método que indica se esse teste pode se servir de otimizações utilizadas no <code>restartBD</code>.
     * 
     * @return <code>true</code> caso o <code>restartBD</code> possa ser otimizado, <code>false</code> caso contrário.
     */
    protected boolean canOptimizeRestartBD() {
        return true;
    }

    /**
     * Devolve o nome do arquivo de spring que deve ser usado pelo teste na inicialização.
     * 
     * @return o nome do arquivo de spring que deve ser usado pelo teste na inicialização.
     */
    protected abstract String getSpringMainXMLFilename();

    /**
     * Usado para inicializar dados que serão utilizados pelos testes.
     * 
     * @throws BeanNotFoundException lançado quando um determinado bean não é encontrado.
     */
    protected abstract void initializeTestData() throws BeanNotFoundException;

    /**
     * Devolve o id no spring do controller principal do teste, que deve ser um {@link PersistenceEnabledController}
     * <p>
     * Por padrão devolve o id do {@link jmine.tec.persist.PlainPersistenceEnabledController}
     * 
     * @return o id no spring do controller principal do teste
     * @see PersistenceEnabledController
     */
    protected String getControllerSpringId() {
        return "plainPersistenceEnabledController";
    }

    /**
     * Descreve o que deve ser executado.
     * 
     * @return Um {@link ReferenceDatabaseDescriptionType}
     */
    public abstract ReferenceDatabaseDescriptionType getReferenceDatabaseDescriptionType();

    /**
     * Este método deve ser implementado e contém a lista de xml's que deve ser importado na execução do teste.
     * 
     * @return lista de xml do tipo db para criação de base de referência.
     */
    protected abstract String[] getResourcesBaseReferencia();
}
